import AssetHandleNode, { AssetHandle } from "./asset_handle_node";
import data from "./cache_data";
import { Col, Collection } from "./data_ext";
import Macro from "./macro";
import NativeLogger from "./native_logger";
import SerializeAble, { CSMemberType, Serialize } from "./serialize";
import Utils from "./utils";
import { Subject } from "./subject";
import { ZipBundle } from "./zip_bundle";
const JSZip = require("jszip");

const { ccclass, property } = cc._decorator;


export enum PreloadType {
    Bundle = 1,
    Remote = 2
};
export class PreloadConfig {
    path: string = "";
    group: string = "";
    type: string = "";
    preloadType: PreloadType = PreloadType.Bundle;

    static Create(path: string, group: string, type: string, preloadType: PreloadType = PreloadType.Bundle) {
        let conf = new PreloadConfig();
        conf.path = path;
        conf.group = group;
        conf.type = type;
        conf.preloadType = preloadType;
        return conf;
    }
    static GetNameByPath(path: string) {
        let name = "";
        name = path.substring(path.lastIndexOf("/") + 1);
        //3.x 版本，spriteFrame texture 是子资源。
        // do {
        //     name = path.substring(path.lastIndexOf("/") + 1);
        //     if (name === "spriteFrame" || name === "texture") {
        //         path = path.substring(0, path.lastIndexOf("/"));
        //     }
        //     else {
        //         break;
        //     }
        // }
        // while (1);
        return name;
    }
};

const DefaultLoad = [
    PreloadConfig.Create("pages/", "pages", "cc.Prefab"),
    PreloadConfig.Create("boxes/", "boxes", "cc.Prefab"),
    PreloadConfig.Create("prefabs/", "prefabs", "cc.Prefab"),
    PreloadConfig.Create("sounds/", "sounds", "cc.AudioClip"),
    PreloadConfig.Create("others/", "others", ""),
    PreloadConfig.Create("librarys/", "librarys", "cc.JsonAsset")
];

export enum LoaderState {
    Idle = 1,
    CheckUpdate,
    Updating,
    LoadBundle,
    LoadBundleOk,
    PreloadAsset,
};
export class BundleConfig extends SerializeAble {
    @Serialize()
    name: string = "";
    @Serialize()
    url: string = "";
    @Serialize()
    version: string = "";
    @Serialize()
    index = 0;
    @Serialize(PreloadConfig, CSMemberType.Array)
    preload: PreloadConfig[] = [];
    @Serialize()
    isZip = false;
};
export class BundleInfo {
    config: BundleConfig;
    from: "origin" | "local" | "web" = "origin";
    abPath: string = "";
    isLoaded = false;
};


export type BundleConfigCollection = Collection<BundleConfig>;
export interface CheckUpdateResult {
    versionRemote: BundleConfigCollection;
    versionLocal: BundleConfigCollection;
    needUpdateList: string[];
};

abstract class LoadPipe extends Subject {
    config: PreloadConfig;
    static CheckAsset(ctor: typeof cc.Asset) {
        //@ts-ignore
        return ctor instanceof cc.Asset && ctor != cc.Texture2D;
    }
    abstract load(): Promise<void>;
};

class LoadPipeRemote extends LoadPipe {
    assetName = "";
    load(): Promise<void> {
        return new Promise<void>(ok => {
            cc.assetManager.loadRemote(this.config.path, (err, asset: cc.Asset) => {
                if (err) {
                    this.emit("fail", err);
                }
                else {
                    let ah = new AssetHandle();
                    ah.asset = asset;
                    ah.name = this.assetName;
                    ah.path = this.config.path;
                    ah.group = this.config.group;
                    this.emit("success", ah);
                }
                ok();
            });
        });
    }
};

class LoadPipeBundle extends LoadPipe {
    bundle: cc.AssetManager.Bundle;

    load() {
        return new Promise<void>(ok => {
            this.bundle.load(this.config.path, cc.Asset, (err, asset: cc.Asset) => {
                if (err) {
                    this.emit("fail", err);
                }
                else {
                    let assetName = PreloadConfig.GetNameByPath(this.config.path);
                    let ah = new AssetHandle();
                    ah.asset = asset;
                    ah.name = assetName;
                    ah.path = this.config.path;
                    ah.group = this.config.group;
                    this.emit("success", ah);
                }
                ok();
            });
        });
    }
};

// const DefaultPreloadDir = ["pages", "boxes", "sounds"];
// const DefaultType = [cc.Prefab, cc.Prefab, cc.AudioClip];

@ccclass
export default class Loader extends AssetHandleNode {
    @property({
        type: [AssetHandle],
        override: true,
        visible: false
    })
    handles: AssetHandle[] = [];
    @property([AssetHandle])
    boxes: AssetHandle[] = [];
    @property([AssetHandle])
    pages: AssetHandle[] = [];
    @property([AssetHandle])
    sounds: AssetHandle[] = [];

    // protected _bundleAssetNameMap: Collection<Collection<AssetHandle>> = {};
    // protected _bundleAssetPathMap: Collection<Collection<AssetHandle>> = {};

    protected onDestroy(): void {
        Loader.__ins = null;
        Utils.loader = null;
    }
    protected onLoad(): void {
        super.onLoad && super.onLoad();
        Loader.__ins = this;
        Utils.loader = this;

        //线程，并发
        // if (cc.sys.isNative) {
        //     cc.assetManager.downloader.maxConcurrency = 2;
        //     cc.assetManager.downloader.maxRequestsPerFrame = 2;
        // }

        cc.assetManager.parser.register(".pak",
            async (file: any, options, onComplete) => {
                let zBundle: ZipBundle = null;
                try {
                    let jszip = JSZip();
                    let zip = await jszip.loadAsync(file);
                    var config = `config.json`;
                    let configJson = JSON.parse(await zip.file(config).async("string"));
                    zBundle = new ZipBundle();
                    zBundle.init(configJson);
                    zBundle.initZip(zip);
                    console.log("zip bundle:", zBundle);
                    // zBundle.load("index.js");

                    if (Macro.BUILD_TARGET !== "dev") {
                        let indexJsBlob = await zBundle.zip.file("index.js").async("blob");
                        let url = URL.createObjectURL(indexJsBlob);
                        //@ts-ignore
                        cc.assetManager.downloader.downloadScript(url, {}, (err, arg) => {
                            console.log("载入脚本index.js", err, arg);
                            URL.revokeObjectURL(url);
                        });
                    }
                    onComplete(null, zBundle);
                }
                catch (err) {
                    onComplete(err, null);
                }
            }
        );

    }
    remap() {
        this.bundles = [];
        // this._bundleAssetNameMap = {};
        // this._bundleAssetPathMap = {};
        this._assetNameMap.clear();
        this._assetTypeMap.clear();
        this._assetGroupMap.clear();
        this._assetPathMap.clear();
        this.handles = []; //todo: loader 的handles 是临时保存AssetHandle。与 AssetHandleNode 中的handles意义不同。
        this.pages.forEach(ah => {
            ah.group = "pages";
            this.remapOne(ah);
        });
        this.boxes.forEach(ah => {
            ah.group = "boxes";
            this.remapOne(ah);
        });
        this.sounds.forEach(ah => {
            ah.group = "sounds";
            this.remapOne(ah);
        });
    }
    private static __ins = null;
    static get ins(): Loader {
        return this.__ins;
    }

    @property(cc.JsonAsset)
    originVersionJson: cc.JsonAsset = null;
    state: LoaderState = LoaderState.Idle;

    //bundle
    bundles: cc.AssetManager.Bundle[] = [];
    //bundle
    getBundles() {
        return cc.assetManager.bundles;
    }
    getBundleByName(name: string) {
        return cc.assetManager.bundles.get(name);
    }
    bundleConfig: BundleConfigCollection = null;
    remotePreloadConfigCol: Col<PreloadConfig>
    //载入
    async init(config: BundleConfigCollection, remotePreloadConfigCol: Col<PreloadConfig>) {
        if (this.state !== LoaderState.Idle) {
            console.warn("[Loader] init 失败，Loader正忙");
            return;
        }

        this.emit("load-start");

        this.emit("log", "准备远程资源列表");
        this.remotePreloadConfigCol = remotePreloadConfigCol;
        this.emit("log", "加载main预设资源");
        this.remap();
        this.emit("log", "完成加载main预设");
        this.emit("log", "加载Bundle包");

        this.state = LoaderState.LoadBundle;
        this.bundleConfig = config;
        //排列载入顺序
        let loadList = [];
        for (let bundleName in this.bundleConfig) {
            loadList.push(this.bundleConfig[bundleName]);
        }
        loadList.sort((a, b) => {
            return a.index - b.index;
        });
        for (let i = 0; i < loadList.length; i++) {
            try {
                await this.loadOneBundle(loadList[i]);
                this.emit("log", `载入bundle包完成：${loadList[i].name}`);
            }
            catch (e) {
                this.emit("log", `载入bundle包失败：${loadList[i].name}`);
            }
        }
        this.emit("log", "加载Bundle包完成");
        this.state = LoaderState.LoadBundleOk;

    }
    async preload() {
        if (this.state !== LoaderState.LoadBundleOk) {
            console.warn("[Loader] preload 失败，还没载入bundle包");
            return;
        }
        this.emit("preload-assets-start");
        this.state = LoaderState.PreloadAsset;
        this.pipes = [];
        this.genPipeBundle();
        this.genPipeRemote();
        this.emit("log", "开始PreloadAsset：");
        await this.preloadPipes();
        this.state = LoaderState.Idle;
        this.emit("log", "载入完成");
        this.emit("load-end");
    }
    pipes: LoadPipe[] = [];
    async preloadPipes() {
        let totalPreloadCount = this.pipes.length;
        let curPreloadCount = 0;
        //开始预载
        NativeLogger.log(`-----开始预载 ${totalPreloadCount} 个资源`);
        this.emit("log", "预加载资源");
        this.emit("progress", 0);
        let pms = [];
        for (let i = 0; i < totalPreloadCount; i++) {
            let pipe = this.pipes[i];
            pipe.on("fail", (err) => {
                NativeLogger.log("preload,fail", pipe.config.path, err);
                curPreloadCount++;
            }, this);
            pipe.on("success", (ah: AssetHandle) => {
                this.setAsset(ah);
                NativeLogger.log("preload,ok", ah.path, ah.group, pipe.config.type);
                this.emit("log", `预加载资源 ${ah.name}(${ah.path})`);
                this.emit("progress", curPreloadCount / totalPreloadCount);
                curPreloadCount++;
            }, this);
            pms.push(pipe.load());
        }
        await Promise.all(pms);
        NativeLogger.log(`-----预载完毕`);
    }
    async genPipeRemote() {
        //
        for (let key in this.remotePreloadConfigCol) {
            let config = this.remotePreloadConfigCol[key];
            let pipe = new LoadPipeRemote();
            pipe.assetName = key;
            pipe.config = config;
            this.pipes.push(pipe);
        }
    }
    async genPipeBundle() {
        let bundleConfig: BundleConfigCollection = this.bundleConfig;

        //收集所有需要preload的bundle与路径。
        for (let bundleName in bundleConfig) {
            let bc = bundleConfig[bundleName];
            let bundle = cc.assetManager.bundles.get(bc.name);
            if (!bundle) {
                continue;
            }

            //默认的预载
            DefaultLoad.forEach(pc => {
                let assetCtor = pc.type ? cc.js.getClassByName(pc.type) : null;
                bundle.getDirWithPath(pc.path).forEach(info => {
                    if (!assetCtor || assetCtor == info.ctor) {
                        let pipe = new LoadPipeBundle();
                        pipe.bundle = bundle;
                        pipe.config = PreloadConfig.Create(info.path, pc.group, pc.type);
                        this.pipes.push(pipe);
                    }
                });
            });

            //配置写的预载。
            for (let i = 0; i < bc.preload.length; i++) {
                let pc: PreloadConfig = bc.preload[i];
                if (bundle) {
                    let assetCtor = pc.type ? cc.js.getClassByName(pc.type) : null;
                    if (pc.path[pc.path.length - 1] == "/") {
                        bundle.getDirWithPath(pc.path).forEach(info => {
                            if (!assetCtor || assetCtor == info.ctor) {
                                let pipe = new LoadPipeBundle();
                                pipe.bundle = bundle;
                                pipe.config = PreloadConfig.Create(info.path, pc.group, pc.type);
                                this.pipes.push(pipe);
                            }
                        });
                    }
                    else {
                        let info = bundle.getInfoWithPath(pc.path);
                        if (!assetCtor || assetCtor == info.ctor) {
                            let pipe = new LoadPipeBundle();
                            pipe.bundle = bundle;
                            pipe.config = pc;
                            this.pipes.push(pipe);
                        }
                    }
                }
            }
        }
    }

    // setAssetWithBundle(ah: AssetHandle, bundle: cc.AssetManager.Bundle) {
    //     this.setAsset(ah);
    //     if (!this._bundleAssetNameMap[bundle.name]) {
    //         this._bundleAssetNameMap[bundle.name] = {};
    //     }
    //     if (!this._bundleAssetPathMap[bundle.name]) {
    //         this._bundleAssetPathMap[bundle.name] = {};
    //     }

    //     this._bundleAssetNameMap[bundle.name][ah.name] = ah;
    //     this._bundleAssetPathMap[bundle.name][ah.path] = ah;

    //     return ah;
    // }

    //todo: cc.SpriteFrame 资源会被识别为 cc.Texture2D
    //todo: 动态加载资源流程 需要优化
    loadAsset<T extends cc.Asset>(assetPathWithMime: string, group: string = "", className: string = "", targetAssetName = "") {
        return new Promise<T>(ok => {
            let assetPath = assetPathWithMime.slice(0, assetPathWithMime.lastIndexOf("."));
            let assetName = targetAssetName || assetPath.slice(assetPath.lastIndexOf("/") + 1);
            let asset = this.getAsset(assetName, group, className);
            if (asset) {
                ok(<T>asset);
                return;
            }
            let type: typeof cc.Asset = <any>cc.js.getClassByName(className);
            let bundle = this.findBundleByPath(assetPath, type);
            if (bundle) {
                bundle.load(assetPath, cc.Asset, (err, asset: cc.Asset) => {
                    if (err) {
                        NativeLogger.log("Loader::loadAsset,fail", assetPath, err);
                        ok(null);
                        return;
                    }
                    let ah = new AssetHandle();
                    ah.asset = asset;
                    ah.name = assetName;
                    ah.path = assetPath;
                    ah.group = group;
                    this.setAsset(ah);
                    ok(<T>asset);
                });
            }
        });
    }

    async loadRemote<T extends cc.Asset>(url: string, cls: new () => T, group: string, assetName: string) {
        let ah: AssetHandle = null;
        let pipe = new LoadPipeRemote();
        pipe.assetName = assetName;
        pipe.once("fail", (err) => {
            NativeLogger.log("Loader::loadRemote error:", err);
            console.error("Loader::loadRemote error:", err);
        }, this);
        pipe.once("success", (asset: T) => {
            ah = new AssetHandle();
            ah.asset = asset;
            ah.name = assetName || asset.name;
            ah.path = url;
            ah.group = group;
            this.setAsset(ah);
        }, this);

        await pipe.load();

        return <T>ah.asset;
    }

    /** 寻找含有XX资源的Bundle */
    public findBundleByPath(path: string, type: typeof cc.Asset) {
        if (!path) {
            cc.error("路径传入错误:", path);
            return null;
        }
        for (let i = this.bundles.length - 1; i >= 0; i--) {
            let bundle = this.bundles[i];
            let info = bundle.getInfoWithPath(path, type);
            if (info) {
                return bundle;
            }
        }
        cc.error("所有包中都没有找到此资源:", path);
        return null;
    }
    public findBundleByPathArray(path: string, type: typeof cc.Asset) {
        if (!path) {
            cc.error("路径传入错误:", path);
            return null;
        }
        let bundleArray = [];
        for (let i = this.bundles.length - 1; i >= 0; i--) {
            let bundle = this.bundles[i];
            let info = bundle.getInfoWithPath(path, type);
            if (info) {
                bundleArray.push(bundle);
            }
        }
        if (bundleArray.length == 0) {
            cc.error("所有包中都没有找到此资源:", path);
        }
        return bundleArray;
    }

    loadOneBundle(bundleConfig: BundleConfig) {
        return new Promise<void>(async (ok, fail) => {
            let loadInfo = new BundleInfo();
            loadInfo.config = bundleConfig;

            loadInfo.abPath = "";
            loadInfo.from = "web";
            data.bundleInfos.push(loadInfo);
            let bundlePath = "";
            if (loadInfo.config.url) {
                bundlePath = `${loadInfo.config.url}/${bundleConfig.name}`;
            }
            else {
                bundlePath = `assets/${bundleConfig.name}`;
            }
            if (loadInfo.config.isZip) {
                let fetchRes = await fetch(`${bundlePath}.pak`);
                cc.assetManager.parser.parse(loadInfo.config.name, await fetchRes.blob(), ".pak", [], (err, bundle) => {
                    console.log(bundle);
                    this.bundles.push(bundle);
                    loadInfo.isLoaded = true;
                    ok();
                });
            }
            else {
                let pathName = window.location.pathname;
                if (/\.html/.test(pathName)) {
                    pathName = pathName.substring(0, pathName.lastIndexOf("/"));
                }
                if (pathName === "") {
                    pathName = "/"
                }
                cc.assetManager.loadBundle(`${window.location.protocol}//${window.location.host}${pathName}${bundlePath}`, (err, bundle: cc.AssetManager.Bundle) => {
                    if (err) {
                        fail(err);
                        return;
                    }
                    this.bundles.push(bundle);
                    loadInfo.isLoaded = true;
                    ok();
                });
            }
        });
    }
};